package com.base.components.common.util;

import com.google.common.collect.Sets;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.io.GeohashUtils;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;

import java.math.BigDecimal;
import java.util.Set;

import static com.base.components.common.util.GeoTool.Level.L_MAX;

/**
 * GeoTool LBS工具，参考文档：
 * <pre>
 *   <a href="https://en.wikipedia.org/wiki/Geohash">wiki</a>
 *   <a href="https://www.jianshu.com/p/7332dcb978b2">中文介绍</a>
 *   <a href="http://geohash.cn/">在线测试</a>
 * </pre>
 *
 * @author <a href="drakelee1221@gmail.com">LiGeng</a>
 * @version 1.0.0, 2018-12-12 20:56
 */
@SuppressWarnings("all")
public class GeoTool {
  private static volatile GeoTool INSTANCE;
  private static final int TOP = 0;
  private static final int RIGHT = 1;
  private static final int BOTTOM = 2;
  private static final int LEFT = 3;

  private static final int EVEN = 0;
  private static final int ODD = 1;

  private String BASE32;
  private String[][] NEIGHBORS;
  private String[][] BORDERS;

  /**
   * 计算geohash
   * @param longitude  X 精度
   * @param latitude   Y 纬度
   * @param level      计算的范围等级
   * @return geohash
   */
  public String encodeGeohash(double longitude, double latitude, Level level){
    return encodeGeohash(longitude, latitude, level == null ? Level.L_MAX.length : level.length);
  }

  /**
   * 解析geohash
   * @param geohash
   * @return
   */
  public Point decodeGeohash(String geohash){
    return GeohashUtils.decode(geohash, SpatialContext.GEO);
  }

  /**
   * 构建一个矩形的坐标范围, 精度低
   * <pre>{@code
   *    Sql示例：
   *      SELECT * FROM table
   *      WHERE (longitude BETWEEN getMinX() AND getMaxX())
   *        AND (latitude BETWEEN getMinY() AND getMaxY());
   * }</pre>
   * @param longitude X 经度
   * @param latitude  Y 纬度
   * @param radius    范围半径 KM
   * @return
   */
  public Rectangle buildRectangleRange(double longitude, double latitude, double radius){
    SpatialContext geo = SpatialContext.GEO;
    return geo.getDistCalc().calcBoxByDistFromPt(
      geo.getShapeFactory().pointXY(longitude, latitude),
      radius * DistanceUtils.KM_TO_DEG, geo, null);
  }

  /**
   * 计算两点之间的距离，单位公里
   * @param longitude1  X 经度1
   * @param latitude1   Y 纬度1
   * @param longitude2  X 经度2
   * @param latitude2   Y 纬度2
   *
   * @return 公里距离
   */
  public double distance(double longitude1, double latitude1, double longitude2, double latitude2) {
    SpatialContext geo = SpatialContext.GEO;
    double distance = SpatialContext.GEO.calcDistance(
      geo.getShapeFactory().pointXY(longitude1, latitude1),
      geo.getShapeFactory().pointXY(longitude2, latitude2));
    return BigDecimal.valueOf(distance).multiply(BigDecimal.valueOf(DistanceUtils.DEG_TO_KM)).doubleValue();
  }

  /**
   * 组装计算距离的Sql字符串
   * <pre>{@code
   *    Sql示例 1：
   *      SELECT *, ${buildSql} dist FROM table
   *      WHERE `geohash_col` like concat(${geohash}, '%')
   *      ORDER BY dist
   *
   *    Sql示例 2：
   *      SELECT *, ${buildSql} dist FROM table
   *      WHERE SUBSTR(`geohash_col`, 1, geohashs[0].length) IN (geohashs)
   *      ORDER BY dist
   * }</pre>
   * @param longitudeCol  经度字段
   * @param latitudeCol   纬度字段
   * @param longitude     当前经度
   * @param latitude      当前纬度
   *
   * @return buildSql
   */
  public String buildDistanceSql(String longitudeCol, String latitudeCol, double longitude, double latitude){
    StringBuffer sql = new StringBuffer("( ");
    sql.append(DistanceUtils.EARTH_EQUATORIAL_RADIUS_KM)
       .append(" * ACOS(")
         .append(" COS( RADIANS( ").append(latitude).append(" ) )")
         .append(" * COS( RADIANS( ").append(latitudeCol).append(" ) )")
         .append(" * COS( RADIANS( ").append(longitudeCol).append(" ) - RADIANS( ").append(longitude).append(" ) )")
         .append(" + SIN( RADIANS( ").append(latitude).append(" ) )")
         .append(" * SIN( RADIANS( ").append(latitudeCol).append(" ) )")
       .append(") )");
    return sql.toString();
  }

  /**
   * 组装计算距离的Sql字符串，同{@link #buildDistanceSql}
   * @param longitudeCol            经度字段
   * @param latitudeCol             纬度字段
   * @param longitudePlaceholder    当前经度参数的占位符，如 ":lon"
   * @param latitudePlaceholder     当前纬度参数的占位符，如 ":lat"
   *
   * @return buildSql
   */
  public String buildDistanceSql(String longitudeCol, String latitudeCol,
                                 String longitudePlaceholder, String latitudePlaceholder){
    StringBuffer sql = new StringBuffer("( ");
    sql.append(DistanceUtils.EARTH_EQUATORIAL_RADIUS_KM)
       .append(" * ACOS(")
         .append(" COS( RADIANS( ").append(latitudePlaceholder).append(" ) )")
         .append(" * COS( RADIANS( ").append(latitudeCol).append(" ) )")
         .append(" * COS( RADIANS( ").append(longitudeCol).append(" ) - RADIANS( ").append(longitudePlaceholder).append(" ) )")
         .append(" + SIN( RADIANS( ").append(latitudePlaceholder).append(" ) )")
         .append(" * SIN( RADIANS( ").append(latitudeCol).append(" ) )")
       .append(") )");
    return sql.toString();
  }

  /**
   * 求与当前geohash相邻的8个格子的geohash值。
   * @param geohash
   * @return string[9]数组，周围格子的geohash值
   */
  public String[] expand(String geohash) {
    String left = calculate(geohash, LEFT);
    String right = calculate(geohash, RIGHT);
    String top = calculate(geohash, TOP);
    String bottom = calculate(geohash, BOTTOM);

    String topLeft = calculate(top, LEFT);
    String topRight = calculate(top, RIGHT);
    String bottomLeft = calculate(bottom, LEFT);
    String bottomRight = calculate(bottom, RIGHT);
    return new String[] {topLeft, top, topRight, left, geohash, right, bottomLeft, bottom, bottomRight};
  }

  /**
   * 递归计算当前区域特定方向的geohash值
   * @param geohash
   * @param direction 偏移方向
   * @return 周围区域的geohash值，超出边界则返回空字符串""
   */
  private String calculate(String geohash, int direction) {
    if ("".equals(geohash)) {     //如果递归到第一个字符仍然处于边界，则不存在这一方向的相邻格子
      return "";
    }
    int length = geohash.length();
    char lastChar = geohash.charAt(length - 1);
    int charType = (geohash.length() % 2) == 1 ? ODD : EVEN;  //最后一位是奇数还是偶数
    String base = geohash.substring(0, length - 1);
    if (BORDERS[charType][direction].indexOf(lastChar) != -1) { //判断对后一位是否处在边界
      base = calculate(base, direction);
    }
    if (!"".equals(base)) {
      return base + NEIGHBORS[charType][direction].charAt(BASE32.indexOf(lastChar));
    } else {
      return "";
    }
  }

  /**
   * 根据半径范围，得到单个用于匹配的geohash栅格，精度较低，匹配数据示例：
   * <pre>{@code
   * Sql示例（右匹配）：
   *    SELECT * FROM table
   *      WHERE `geohash_col` like concat(${geohash}, '%')
   * }</pre>
   * @param longitude X 经度
   * @param latitude  Y 纬度
   * @param radius    范围半径 KM
   *
   * @return geohash栅格
   */
  public String singleGeohash(double longitude, double latitude, double radius){
    return preciseGeohash(longitude, latitude, radius, 0).iterator().next();
  }

  /**
   * <p>根据半径范围，得到单个用于匹配的geohash栅格，根据 Level 中的栅格层数对应的半径范围，
   * <p>栅格层数越多匹配越精确（返回的栅格数越多），返回的栅格个数为：(2 x 栅格层数 + 1)² ；
   * <p>超过最大栅格层数对应的半径范围时，会使用上一级 Level，同时只返回一个栅格，参考{@link #singleGeohash}；
   * <p>如： 半径为10公里，则会返回 Level_5，栅格层数为2，返回栅格数为25个。
   * <pre>{@code
   * Sql示例（右匹配）：
   *    SELECT * FROM table
   *      WHERE SUBSTR(`geohash_col`, 1, geohashs[0].length) in (geohashs)
   *
   * }</pre>
   * @param longitude X 经度
   * @param latitude  Y 纬度
   * @param radius    范围半径 KM
   *
   * @return 栅格集合，每个geohash的长度是一样，至少会返回一个
   */
  public Set<String> preciseGeohash(double longitude, double latitude, double radius){
    return preciseGeohash(longitude, latitude, radius, -1);
  }

  /**
   * 参考{@link #preciseGeohash}
   * @param longitude      X 经度
   * @param latitude       Y 纬度
   * @param radius         范围半径 KM
   * @param maxTableRound  指定最大的栅格层数，值小于 0 时使用Level默认值
   *
   * @return
   */
  public Set<String> preciseGeohash(double longitude, double latitude, double radius, int maxTableRound){
    Precision precise = precise(longitude, latitude, radius, maxTableRound);
    Set<String> hashs = Sets.newHashSet();
    String localGeohash = encodeGeohash(longitude, latitude, precise.length);
    hashs.add(localGeohash);
    for (int i = 1; i <= precise.tableRound; i++) {
      Set<String> sets = Sets.newLinkedHashSet();
      for (String h : hashs) {
        for (String hash : expand(h)) {
          sets.add(hash);
        }
      }
      hashs.addAll(sets);
    }
    return hashs;
  }

  public Precision precise(double x, double y, double radius, int tableRound){
    double preRangeKm = DistanceUtils.EARTH_MEAN_RADIUS_KM;
    Set<String> hashs = Sets.newLinkedHashSet();
    int length = 0;
    Level pre = L_MAX;
    // length 从低到高循环
    for (Level r : Level.values()) {
      // 目标半径 比当前 Level 范围大 且 比 上一个 Level 范围小
      if( r.rangeKm <= radius && radius <= preRangeKm ){
        // 不超过当前 Level 的最大栅格层数
        int round = r.maxTableRound < tableRound || tableRound < 0 ? r.maxTableRound : tableRound;
        for (int i = 0; i <= round; i++) {
          double rangeKm = r.computeRangeKm(i);
          if(radius <= rangeKm){
            return new Precision(r.length, i);
          }
        }
        return new Precision(pre.length, 0);
      }
      pre = r;
    }
    return new Precision(L_MAX.length, 0);
  }

  /** 范围等级 */
  public static enum Level {
    L_MIN(1, 2500, 0),

    /** 栅格层数 = 半径范围 ： 1 = 1890.0 */
    L_2(2, 630, 1),

    /** 栅格层数 = 半径范围 ： 1 = 234.0，2 = 390.0 */
    L_3(3, 78, 2),

    /** 栅格层数 = 半径范围 ： 1 = 60.0	 */
    L_4(4, 20, 1),

    /** 栅格层数 = 半径范围 ： 1 = 7.2，2 = 12.0，3 = 16.8 */
    L_5(5, 2.4, 3),

    /** 栅格层数 = 半径范围 ： 1 = 1.83 */
    L_6(6, 0.61, 1),

    /** 栅格层数 = 半径范围 ： 1 = 0.228，2 = 0.38 */
    L_7(7, 0.076, 2),

    /** 栅格层数 = 半径范围 ： 1 = 0.05733 */
    L_8(8, 0.01911, 1),

    /** 栅格层数 = 半径范围 ： 1 = 0.01434 */
    L_9(9, 0.00478, 1),

    /** 栅格层数 = 半径范围 ： 1 = 0.0017913，2 = 0.0029855 */
    L_10(10, 0.0005971, 2),

    /** 栅格层数 = 半径范围 ： 1 = 0.0004476 */
    L_11(11, 0.0001492, 1),

    /** 栅格层数 = 半径范围 ： 1 = 0.0000558，2 = 0.000093 */
    L_MAX(12, 0.0000186, 2);

    private Level(int length, double rangeKm, int maxTableRound) {
      this.length = length;
      this.rangeKm = rangeKm;
      this.maxTableRound = maxTableRound;
    }
    /** geohash长度 */
    private int length;
    /** 半径范围公里 */
    private double rangeKm;
    /** 根据父级长度公里范围的相差值的中间值，计算出相近的最大的外圈栅格层数 */
    private int maxTableRound;

    /** 计算当前 Level 最大栅格层数的半径范围，公式： 默认半径 x (2 x 最大栅格层数 + 1)  */
    public double computeMaxTableRoundRangeKm() {
      return computeRangeKm(maxTableRound);
    }
    /** 根据 栅格层数 计算当前 Level 的半径范围，公式： 默认半径 x (2 x 栅格层数 + 1)   */
    public double computeRangeKm(int tabelRound){
      if(tabelRound <= 0){
        return rangeKm;
      }
      return BigDecimal.valueOf(rangeKm).multiply(BigDecimal.valueOf(2 * tabelRound + 1)).doubleValue();
    }
    public int getLength() {
      return length;
    }
    public double getRangeKm() {
      return rangeKm;
    }
    public int getMaxTableRound() {
      return maxTableRound;
    }
  }

  /** 精度 */
  public static class Precision{
    /** geohash 长度 */
    final public int length;
    /** 栅格层数 */
    final public int tableRound;
    public Precision(int length, int tableRound) {
      this.length = length;
      this.tableRound = tableRound;
    }
  }

  public static GeoTool instance(){
    if (INSTANCE == null) {
      synchronized (GeoTool.class) {
        if (INSTANCE == null) {
          INSTANCE = new GeoTool();
        }
      }
    }
    return INSTANCE;
  }

  private String encodeGeohash(double longitude, double latitude, int length){
    return GeohashUtils.encodeLatLon(latitude, longitude, length);
  }

  private GeoTool() {
    BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";
    NEIGHBORS = new String[2][4];
    BORDERS = new String[2][4];

    BORDERS[ODD][TOP] = "bcfguvyz";
    BORDERS[ODD][RIGHT] = "prxz";
    BORDERS[ODD][BOTTOM] = "0145hjnp";
    BORDERS[ODD][LEFT] = "028b";

    BORDERS[EVEN][TOP] = BORDERS[ODD][RIGHT];
    BORDERS[EVEN][RIGHT] = BORDERS[ODD][TOP];
    BORDERS[EVEN][BOTTOM] = BORDERS[ODD][LEFT];
    BORDERS[EVEN][LEFT] = BORDERS[ODD][BOTTOM];

    NEIGHBORS[ODD][TOP] = "238967debc01fg45kmstqrwxuvhjyznp";
    NEIGHBORS[ODD][RIGHT] = "14365h7k9dcfesgujnmqp0r2twvyx8zb";
    NEIGHBORS[ODD][BOTTOM] = "bc01fg45238967deuvhjyznpkmstqrwx";
    NEIGHBORS[ODD][LEFT] = "p0r21436x8zb9dcf5h7kjnmqesgutwvy";

    NEIGHBORS[EVEN][TOP] = NEIGHBORS[ODD][RIGHT];
    NEIGHBORS[EVEN][RIGHT] = NEIGHBORS[ODD][TOP];
    NEIGHBORS[EVEN][BOTTOM] = NEIGHBORS[ODD][LEFT];
    NEIGHBORS[EVEN][LEFT] = NEIGHBORS[ODD][BOTTOM];
  }
}
